import java.io.File;
import java.io.FileWriter;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Scanner;

class FileSystem {
	int MASK[] = new int[32];
	OFTEntry[] OFT;
	IOSystem io;

	public FileSystem() {
		io = new IOSystem();
		initMASK();
		initOFT();
	}

	private void initOFT() {
		OFT = new OFTEntry[4];
		for(int i=0; i<4; i++){
			OFT[i] = new OFTEntry(i);
		}	
	}

	public String createFile(String fileName) {
		int freeDescriptor = -1;
		//no more free file entries, need to load new block to directory
		int[] dirDescriptor = io.readBlock(1);
		
		//check whether there is a created file with same name
		int targetFileName = convertFileNameToInt(fileName);
		for (int i = 1; i < 4; i++) {
			int[] dirBlock = io.readBlock(dirDescriptor[i]);
			for (int j = 0; j < 16; j += 2) {
				// found the target file
				if (dirBlock[j] == targetFileName) {
					return "error";
				}
			}
		}
		
		if (dirDescriptor[0] % 8 == 0) {
			// assign a new block to directory
			int newBlkNum = findFreeBlock();
			dirDescriptor[dirDescriptor[0]/8 + 1] = newBlkNum;

			// mark all directory entries in the new block as empty
			int[] dirBlock = io.readBlock(newBlkNum);
			for (int i = 0; i < 16; i += 2) {
				dirBlock[i] = -1;
			}

			// write data back to ldisk
			io.writeBlock(newBlkNum, dirBlock);
			// write directory descriptor back to ldisk
			io.writeBlock(1, dirDescriptor);
		}

		// find a free file descriptor
		boolean found = false;
		for (int i = 1; i < 7 && !found; i++) {
			int[] fileDescriptor = io.readBlock(i);
			
			for (int j = 0; j < 16; j += 4) {
				// found a free file descriptor
				if (fileDescriptor[j] < 0) {
					fileDescriptor[j] = 0;
					freeDescriptor = (i - 1) * 4 + j / 4; //free descriptor is in block (i-1), index j
					System.out.println("free descriptor: " + freeDescriptor);
					io.writeBlock(i, fileDescriptor);
					found = true;
					break;
				}
			}
		}

		// find a free directory entry
		found = false;
		for (int i = 1; i < 4 && !found; i++) {
			int[] dirEntry = io.readBlock(dirDescriptor[i]);
			
			for (int j = 0; j < 16; j += 2) {
				// found a free directory entry
				if (dirEntry[j] == -1) {
					// store file name to the free directory entry
					byte[] temp = new byte[4];
					byte[] byteFileName = fileName.getBytes();
					System.arraycopy(byteFileName, 0, temp, 0, byteFileName.length);
					ByteBuffer fileNameBuffer = ByteBuffer.allocate(4);
					fileNameBuffer.put(temp);
					fileNameBuffer.flip();
					dirEntry[j] = fileNameBuffer.getInt();

					// link to the file descriptor
					dirEntry[j + 1] = freeDescriptor;
					
					// update dirctory length
					dirDescriptor[0]++;

					// write data back to ldisk
					io.writeBlock(1, dirDescriptor);
					io.writeBlock(dirDescriptor[i], dirEntry);
					
					found = true;
					break;
				}
			}
		}

		return fileName + " created";
	}

	public String deleteFile(String fileName) {
		//convert file name to integer
		int targetFileName = convertFileNameToInt(fileName);

		int[] dirDescriptor = io.readBlock(1);
		for (int i = 1; i < 4; i++) {
			int[] dirBlock = io.readBlock(dirDescriptor[i]);
			
			for (int j = 0; j < 16; j += 2) {
				// found the target file
				if (dirBlock[j] == targetFileName) {
					int fileDescriptor = dirBlock[j + 1];
					System.out.println("target file descriptor found: " + dirBlock[j + 1]);
					
					// file is open; 
					for (int k = 1; k < OFT.length; k++) {
							if (!OFT[k].free 
									&& OFT[k].descriptorIndex == dirBlock[j+1]){
								closeFile(k);
								break;
							}
					}
					
					// free directory entry
					dirBlock[j] = -1;
					dirBlock[j + 1] = 0;

					// free file descriptor
					int blockNum = fileDescriptor / 4 + 1;
					int descriptorNum = fileDescriptor % 4;
					
					int[] fileDescriptorBlock = io.readBlock(blockNum);
					if (fileDescriptorBlock[descriptorNum * 4] != -1) {
						// update bit map if the file is not empty
						for (int k = 1; k < 4; k++) {
							if (fileDescriptorBlock[descriptorNum * 4 + k] != 0) {
								int[] BM = Arrays.copyOfRange(io.readBlock(0),0, 2);
								//determine bit map 0 or 1
								int BMNum = fileDescriptorBlock[descriptorNum * 4 + k] / 32;
								//compute which bit to free
								int MASKNum = fileDescriptorBlock[descriptorNum * 4 + k] % 32;
								BM[BMNum] = BM[BMNum] & ~MASK[MASKNum];
								
								//write the updated bit map to disk
								int[] block0 = io.readBlock(0);
								System.arraycopy(BM, 0, block0, 0, 2);
								io.writeBlock(0, block0);
							}
						}
						//set the file length in file descriptor to -1
						fileDescriptorBlock[descriptorNum * 4] = -1;

					}
					return fileName + " destroyed";
				}
			}
		}

		return "error";
	}

	/**
	 * @param fileName
	 * @return
	 */
	private int convertFileNameToInt(String fileName) {
		byte[] temp = new byte[4];
		byte[] fileNameByte = fileName.getBytes();
		System.arraycopy(fileNameByte, 0, temp, 0, fileNameByte.length);
		
		ByteBuffer fileNameBuffer = ByteBuffer.allocate(4);
		fileNameBuffer.put(temp);
		fileNameBuffer.flip();
		int targetFileName = fileNameBuffer.getInt();
		
		return targetFileName;
	}

	public int openFile(String fileName) {
		//convert file name to int
		int targetFileName = convertFileNameToInt(fileName);
		
		int[] dirDescriptor = io.readBlock(1);
		for(int i=1; i<4; i++){
			int[] dirBlock = io.readBlock(dirDescriptor[i]);
			for(int j=0; j<16; j+=2){
				//found the file name
				if(dirBlock[j] == targetFileName){
					OFTEntry freeEntry = findFreeEntry();
					
					// no free entry == error?
					if (freeEntry == null || this.checkOpened(dirBlock[j+1])) {
						return -1;
					}
					
					freeEntry.free = false;
					freeEntry.currentPos = 0;
					freeEntry.descriptorIndex = dirBlock[j+1];
					//compute the block number of the file descriptor
					int blockNum = dirBlock[j+1] / 4 + 1;
					int descriptorNum = dirBlock[j+1] % 4;
					//read the block which contains the file descriptor
					int[] fileDescriptor = io.readBlock(blockNum);
					//read the first data block of the file
					int[] buffer = io.readBlock(fileDescriptor[descriptorNum * 4 + 1]);
					//set the file length in oft entry
					freeEntry.fileLen = fileDescriptor[descriptorNum * 4];
					//fill in the buffer in the oft entry
					freeEntry.buffer = buffer;
					
					return freeEntry.index;
				}
			}
		}
		return -1;
	}

	private boolean checkOpened(int block) {
		for (int i=1; i < OFT.length; i++) {
			if (!OFT[i].free
					&& OFT[i].descriptorIndex == block) {
				return true;
			}
		}
			
		return false;
	}
	
	private OFTEntry findFreeEntry() {
		//open file table entry 0 reserved for directory
		for(int i=1; i<4; i++){
			if(OFT[i].free)
				return OFT[i];
		}
		return null;
	}

	public String closeFile(int index) {
		//compute the block number of the file descriptor
		if (index < 1 || index > 3 || OFT[index].free) {
			return "error";
		}
		
		int blockNum = OFT[index].descriptorIndex / 4 + 1;
		int descriptorNum = OFT[index].descriptorIndex % 4;
		
		//read the block which contains file descriptor
		int[] fileBlock = io.readBlock(blockNum);
		
		//write buffer to disk
		int bufferBlockNum = fileBlock[(descriptorNum * 4 + 1) + (OFT[index].currentPos / 64)];
		io.writeBlock(bufferBlockNum, OFT[index].buffer);
		
		//update file length
		fileBlock[descriptorNum * 4] = OFT[index].fileLen;
		io.writeBlock(blockNum, fileBlock);
		
		//free oft entry
		OFT[index].free = true;
		
		return index + " closed";
	}

	public String readFile(int index, int count) {
		StringBuilder sb = new StringBuilder();
		//convert the integer array to byte array
		byte[] blockData = convertIntToByte(OFT[index].buffer);

		//compute position in the r/w buffer
		int pos = OFT[index].currentPos + count;
		
		//position is larger than total length of the file
		if(pos > OFT[index].fileLen){
			return "error";
		}
		
		System.out.println("current position is: " + OFT[index].currentPos);
		
		//position is not within current block
		while(OFT[index].currentPos < pos){
			//all content in current block has been read, need to load next block to r/w buffer
			if(OFT[index].currentPos % 64 == 0 && OFT[index].currentPos < pos){
				//compute block number and index of the file descriptor
				int blockNum = OFT[index].descriptorIndex / 4 + 1;
				int descriptorNum = OFT[index].descriptorIndex % 4;
				
				int[] descriptorBlock = io.readBlock(blockNum);
				int nextBlkNum = descriptorNum * 4 + (OFT[index].currentPos/64 + 1);
				OFT[index].buffer = io.readBlock(descriptorBlock[nextBlkNum]);
				//convert the integer array to byte array
				blockData = convertIntToByte(OFT[index].buffer);
			}
			
			//compute available length
			int len = (OFT[index].currentPos / 64 + 1) * 64 - OFT[index].currentPos;
			//available length is larger than count
			if(len > count){
				len = count;
			}
			
			//read all data in current block
			int i = OFT[index].currentPos % 64;
			for(; i< (OFT[index].currentPos % 64 + len) && i<blockData.length; i++){
				String dataString = new String(Arrays.copyOfRange(blockData, i, i+1));
				sb.append(dataString);
			}
			
			//update current position
			OFT[index].currentPos += len;
			//update count left
			count -= len;
		}
		
		return sb.toString();
	}

	private byte[] convertIntToByte(int[] buffer) {
		byte[] blockData = new byte[64];
		for(int i=0; i<buffer.length; i++){
			byte[] data = ByteBuffer.allocate(4).putInt(buffer[i]).array();
			System.arraycopy(data, 0, blockData, i*4, 4);
		}
		return blockData;
	}

	public String writeToFile(int index, char data, int count) {
		//compute position in the r/w buffer
		int pos = OFT[index].currentPos + count;
		
		//position is larger than length of 3 block data, return error
		//if the file didn't open yet, return error
		if(pos >= 191 || OFT[index].free){
			return "error";
		}
		
		//convert char array to byte array
		char[] content = new char[count];
		for(int i=0; i< count; i++){
			content[i] = data;
		}
		String contentString = new String(content);
		byte[] contentByte = contentString.getBytes();
		
		//convert integer array inside the buffer to byte array
		byte[] byteBuffer = convertIntToByte(OFT[index].buffer);
		
		// compute the block number and index of the file descriptor
		int blockNum = OFT[index].descriptorIndex / 4 + 1;
		int descriptorNum = OFT[index].descriptorIndex % 4;
		
		// read the block which contains file descriptor
		int[] fileBlock = io.readBlock(blockNum);
		
		
		int tempCount = count;
		while(OFT[index].currentPos < pos){
			// need to allocate new block to the file
			int availableLen;
			if(OFT[index].fileLen == 0 && OFT[index].currentPos == 0){
				availableLen = 0;
			} else {
				availableLen = (Math.max(OFT[index].fileLen, OFT[index].currentPos) / 64 + 1) * 64 - 1;
			}
			
//			System.out.println("available length is: " + availableLen);
//			System.out.println("current position is: " + OFT[index].currentPos);
			
			if (availableLen < pos) {
				int freeBlkNum = findFreeBlock();
				int newIndex = descriptorNum * 4 + 1 + (int)Math.ceil(Math.max(OFT[index].fileLen, OFT[index].currentPos) / 64.0);
				System.out.println("free block number is: " + freeBlkNum);
				fileBlock[newIndex] = freeBlkNum;
				io.writeBlock(blockNum, fileBlock);
			}
			
			//need to load next block
			if(OFT[index].currentPos % 64 == 0 && OFT[index].currentPos < pos){
				int nextBlkNum = fileBlock[descriptorNum * 4 + 1 + OFT[index].currentPos / 64];
				OFT[index].buffer = io.readBlock(nextBlkNum);
			}
			
			// available length in current block
			int len = (OFT[index].currentPos / 64 + 1) * 64 - OFT[index].currentPos;
			//if available length if current block is larger than count, then write all data to current block
			if(len >= tempCount){
				len = tempCount;
			}
			
			System.arraycopy(contentByte, 0, byteBuffer,
					OFT[index].currentPos % 64, len);
			OFT[index].currentPos += len;
			tempCount -= len;
			OFT[index].buffer = convertByteToInt(byteBuffer);
			
			// write the buffer back to disk
			int dataBlkNum = fileBlock[descriptorNum * 4 + 1 + (OFT[index].currentPos-1) / 64];
			//System.out.println("data block is: " + dataBlkNum);
			io.writeBlock(dataBlkNum, OFT[index].buffer);
			
		}
		
		//update file len
		if(OFT[index].fileLen < pos){
			OFT[index].fileLen = pos;
			fileBlock[descriptorNum * 4] = pos;
			io.writeBlock(blockNum, fileBlock);
		}
		
		return count + " bytes written";
	}

	private int findFreeBlock() {
		int[] BM = io.readBlock(0);
		for(int i = 0; i<2; i++){
			for(int j=0; j<32; j++){
				int test = BM[i] & MASK[j];
				if(test == 0 && !(i == 0 && j < 7)){ //free block cannot be the first 7 block. i.e ldisk[0] - ldisk[6]
					//update bit map
					BM[i] = BM[i] | MASK[j];
					
					//write the updated bit map to disk
					int[] block0 = new int[16];
					System.arraycopy(BM, 0, block0, 0, 2);
					io.writeBlock(0, block0);
					
					return (i* 32) + j;
				}
			}
		}
		return -1;
	}

	private int[] convertByteToInt(byte[] byteBuffer) {
		int[] buffer = new int[16];
		byte[] temp = new byte[64];
		System.arraycopy(byteBuffer, 0, temp, 0, byteBuffer.length);

		for (int i = 0; i < 16; i++) {
			ByteBuffer dataBuffer = ByteBuffer.allocate(4);
			dataBuffer.put(Arrays.copyOfRange(temp, i * 4, i * 4 + 4));
			dataBuffer.flip();
			buffer[i] = dataBuffer.getInt();
		}

		return buffer;
	}

	public String seek(int index, int pos) {
		//new position is out of bound
		if (pos < 0 || index < 1 || index > 3 || pos > OFT[index].fileLen ||OFT[index].free)
			return "error";
		
		//new position is not within current block		
		if((OFT[index].currentPos/64 + 1) * 64 - 1 < pos || OFT[index].currentPos/64 * 64 > pos){
			//write buffer to disk
			int blockNum = OFT[index].descriptorIndex / 4 + 1;
			int descriptorNum = OFT[index].descriptorIndex % 4;
			int[] descriptorBlock = io.readBlock(blockNum);
			io.writeBlock(descriptorBlock[descriptorNum * 4
			                              + (OFT[index].currentPos / 64 + 1)],
					OFT[index].buffer);
			
			//read the new block
			int newBlockNum = descriptorBlock[descriptorNum * 4 + (pos/64 + 1)];
			OFT[index].buffer = io.readBlock(newBlockNum);
			
			//set the current position to the new position
			OFT[index].currentPos = pos;
			
		} else {
			OFT[index].currentPos = pos;
		}
		
		return "position is " + pos;
	}

	public String directory() {
		StringBuilder sb = new StringBuilder();
		int[] dirDescriptor = io.readBlock(1);
		for (int i = 1; i < 4; i++) {
			int[] dirBlock = io.readBlock(dirDescriptor[i]);
			for (int j = 0; j < 16 && dirDescriptor[i] != 0; j += 2) {
				// non-empty file name
				if (dirBlock[j] != -1) {
					byte[] fileNameByte = ByteBuffer.allocate(4)
							.putInt(dirBlock[j]).array();
					String fileName = (new String(fileNameByte)).trim();
					// check whether there is a file
					if (!fileName.equals("")) {
						sb.append(fileName + " ");
					}
				}
			}
		}

		return sb.toString();
	}

	public String init(String fileName) {
		if (fileName == null) {
			initLdisk();
			initOFT();
			return "disk initialized";
		} else {
			File file = new File(fileName);
			if (!file.exists()) {
				return this.init(null);
			}
			else {
				io = new IOSystem();
				initLdisk();
				initOFT();
				
				// restore saved file
				try {
					int[] temp = new int[16];
					Scanner scan = new Scanner(file);
					for(int j=0; j<64 && scan.hasNext(); j++){
						for(int i=0; i<16; i++){
							temp[i] = scan.nextInt();
						}
						io.writeBlock(j, temp);
					}
					scan.close();
				} catch (Exception e) {
					e.printStackTrace();
					return "error";
				}
			}
		}
		
		return "disk restored";
	}

	private void initLdisk() {
		// initial BM
		io.writeBlock(0, new int[16]);

		// 0 in directory length; 16 bytes per descriptor
		int[] dirDescriptor = new int[16];
		dirDescriptor[0] = 0;
		io.writeBlock(1, dirDescriptor);

		// mark rest of file descriptor length as -1
		for (int i = 1; i < 7; i++) {
			int[] block = io.readBlock(i);

			for (int j = 0; j < 16; j += 4) {
				if(j==0 && i ==1)
					continue;
				block[j] = -1;
			}

			io.writeBlock(i, block);
		}
		
		// open directory in oft table
		dirDescriptor = io.readBlock(1);
		int[] dirBlock = io.readBlock(dirDescriptor[1]);
		OFT[0] = new OFTEntry(dirBlock, 0, 0, dirDescriptor[0]);
	}

	public String save(String fileName) {
		try {
			File file = new File(fileName);
			if (!file.exists()) {
				file.createNewFile();
			}
			FileWriter fw = new FileWriter(file);
			fw.write("");
			for(int i=0; i<64; i++){
				int[] block = io.readBlock(i);
				for(int j=0; j<16; j++){
					fw.append(block[j] + " ");
				}
			}
			fw.close();
		} catch (Exception e) {
			return "error";
		}
		return "disk saved";
	}

	private void initMASK() {
		MASK[31] = 1;
		for (int i = 30; i >= 0; i--) {
			MASK[i] = MASK[i + 1] << 1;
		}
	}

}
